Imports:

show object:

Generate QR-Code's for Demo and Github:

ImageDisplayer objects:

New in PyTeal: Boxes and ABI-Compatible Smart Contracts¶

image.png

Learn how to utilize unlimited global storage and ABI-compatibility in PyTeal, Algorand’s library for writing smart contracts in Python.¶

Agenda
¶

I. PyTeal¶

II. Application Boxes in PyTeal¶

III. PyTeal's ABI-Router: Building an ARC-4 Application¶

IV. Demo¶

V. Questions¶

image.png

.

References
¶

Decipher 2021: Writing Smart Contracts with Python¶

https://www.github.com/jasonpaulos/decipher-22-pyteal-talk¶

In [6]:
github_qr_code()

image.png

.

PyTeal
¶

image.png

.

image.png

.

image.png

image.png

.

Polling App
¶

image.png

Favorite Tall Building?¶

1. Empire State - New York¶

2. Burj Khalifa - Dubai¶

3. Abraj Al-Bait - Mecca¶

4. Taipei 101 - Taipei¶

5. Shanghai Tower - Shanghai¶

6. Merdeka 118 - Kuala Lumpur¶

7. Other¶

image.png

.

Let's Take a Poll On Chain¶

image.png

.

Poll administrator can:¶

create and delete the poll application¶

open and close a poll for voting¶

Anyone can:¶

submit a choice¶

get the poll status¶

image.png

.

Example¶

submit choice Burj Khalifa - Dubai¶

After 100 submissions¶

status =¶

  • 11 chose Empire State - New York
  • 35 chose Burj Khalifa - Dubai
  • 22 chose Abraj Al-Bait - Mecca
  • ...

image.png

.

In [7]:
%%script false --no-raise-error 

# Dead End

%pip install networkx
%pip install matplotlib

import networkx as nx
import matplotlib.pyplot as poll
import random
from networkx.algorithms import bipartite
CHOICES = 7
LAST = 10

choices = list(range(CHOICES))
accounts = [f"{' '*10}ACC {chr(x)}" for x in range(ord('A'), ord('A') + LAST)]

B = nx.Graph()
B.add_nodes_from(choices, bipartite=0)
B.add_nodes_from(accounts, bipartite=1)

# print(choices, accounts)
B.add_edges_from([(random.randint(0, CHOICES-1), acc) for acc in accounts])
pos = dict()
pos.update( (n, (1, LAST - i)) for i, n in enumerate(choices) ) 
pos.update( (n, (2, LAST - i)) for i, n in enumerate(accounts) ) 

# print(pos)
nx.draw_networkx(B, pos=pos, node_size=200)
nx.draw_networkx_nodes(B, pos=pos, nodelist=choices, node_size=300)
poll.axis('off')
poll.show()

Voting accounts must contribute exactly +1 to tally¶

image.png

image.png

.

Voting accounts must contribute exactly +1 to tally¶

image.png

image.png

.

Voting accounts must contribute exactly +1 to tally¶

image.png

image.png

.

NOTES:

  • Can't keep all this info in global storage
    • Stats - 8KB total possible. Assuming no compression and 32B per address, that's at most 256 accounts we can keep track of
  • Can keep info in local - but then there are some downsides:
    • voters need to opt into poll (MBR issues)
    • need to have policies and code around closing/clearing out
  • TRICKY Sybil attack. Why can't some create tons of accounts and vote many times. AND it's even easier to pull that off with the reduced MBR requirements thanks to boxes...
    • Answer: yes, this isn't meant to be a solution to the general problem of governance voting, etc.
    • This is only meant for low-stake polls

Voting accounts must contribute exactly +1 to tally¶

HOW ?
¶

Can we store each account in global storage?¶

https://github.com/algorand/go-algorand/blob/995ae47e80c50e7632034cac8a70b7d6434d03e3/config/consensus.go#L969-L970

64 keys X (64 + 64 bytes) == 8,196 bytes

Yes, BUT limited to 8KB which is only 256 accounts¶

image.png

.

Voting accounts must contribute exactly +1 to tally¶

HOW ?
¶

Can we keep track of an account's vote in local storage?¶

Yes, BUT requires opt in¶

clear state forcibly erases submission record¶

min MBR = 0.15 Algos with 1 local¶

complex code/policies for opt in, close out, clear state¶

image.png

.

Voting accounts must contribute exactly +1 to tally¶

Anything else we could do?¶

Introduce voting token¶

Yes, BUT requires asset opt-in and aiming for lightweight app¶

UNLIMITED assets: hack a new asset for each account without opt-in¶

complicated, error prone, expensive¶

Notes:

  • expensive 0.1 Algos / acct vs. 0.0157 Algos / acct w/ boxes
  • complicated / error prone
    • need to keep track of asset id off chain and supply it as a foreign ref, then parse out URL, verify that first 32 bytes are the account, etc...
    • need to group with Asset Config Txn which specifies how the URL changes

image.png

.

SOLUTION via Application Boxes
¶

image.png

Notes:

  • In this solution, only need
    • 32 bytes for the Key (max is 64 bytes)
    • 1 byte for the value (max is 32 KB)
  • Costs in terms of MBR formula of 2500 + 400*(len(key) + len(value))
    • 15.7 mili-Algo's to store submission info per account
    • Each txn allows up to 8 box references in it with the size of boxes touched totalling at most 1KB * # of refs
    • In our case, this is not an issue. A single box reference -being the submission account- will suffice

image.png

.

Application Boxes in PyTeal
¶

image.png

.

Notes

  • create: create a box of specified size, with a NoOp if already exists. Returns an indicator for whether box actually got created
  • length: when the box exists, gives its value's size. When it doesn't its hasValue() will be false
  • delete: don't forget to do this as apps don't clean up their own boxes!!! Returns indicator for whether box actually got deleted
  • replace: ... nothing to add ...
  • extract: ... nothing to add ...
  • put and get: the workhorses we'll see in action shortly
  • put: for setting the entire contents of a box, creating it when it doesn't exist. Returns indicator for whether a box was created during execution
  • get: get the entire contents of a box. hasValue() will be false if the box doesn't actually exist

PyTeal App.box_*() API
¶

App.box_create(name, size) - any size ($\leq$ 32KB)¶

App.box_length(name) - opcode box_len¶

App.box_delete(name) - opcode box_del¶

App.box_replace(name, idx, L) - set part of box¶

App.box_extract(name, idx, L) - get part of box¶

App.box_put(name, value) - set value (may create box)¶

App.box_get(name) - get everything (fail if size > 4KB)¶

image.png

.

App.box_get() and App.box_put()¶

Notes

Inspired by

@router.method
def submit(choice: abi.Uint8) -> Expr:
  • sender_box is a MaybeValue:
    • sender_box.has_value() <--> box exists
    • sender_box.value() --> (when box exists) full contents
In [8]:
from pyteal import Int, Itob, Txn, App, Seq, If, Assert, Btoi
# App.box:    account --> choice
# App.global: choice  --> count
num_choices = Int(7)
choice = Itob(Int(1))  # index for "Burj Khalifa - Dubai"
box_key = Txn.sender()          # sender's account
previous = App.box_get(box_key) # MaybeValue
submit_expr = Seq(
  Assert(Btoi(choice) < num_choices),
  If(previous.hasValue()).Then(
    App.globalPut(      # App.global[previous] -= 1 :
      previous.value(),
      App.globalGet(previous.value()) - Int(1)
    ),
  ),
  App.box_put(box_key, choice),  # App.box[box_key] <-- choice
  # App.global[choice] += 1 :
  App.globalPut(choice, App.globalGet(choice) + Int(1)),
)

image.png

.

PyTeal's ABI-Router: Building an ARC-4 Application¶

image.png

.

methods - allow interacting with Poll App¶

* create - called only during app creation and only by administrator¶

* open - called only by administrator¶

* close - called only by administrator¶

* submit¶

* status¶

(didn't include delete for reasons to be explained shortly)¶

image.png

.

Refresher - OnComplete Actions for App Transactions¶

image.png

Value Name Description
0 NoOp Execute ApprovalProgram only
1 OptIn Allocate local state and execute ApprovalProgram
2 CloseOut Execute ApprovalProgram and clear local state
3 ClearState Execute ClearStateProgram and clear locals (even if rejects)
4 UpdateApplication Execute ApprovalProgram and update programs
5 DeleteApplication Execute ApprovalProgram and delete the app

image.png

.

Definition: method¶

An ABI method is a section of code called externally through an application transaction whose first argument is the method's selector¶

Definition: Bare app call action¶

An ABI bare app call is an application transaction with zero arguments and its associated action is the section of code to be executed¶

image.png

.

Definition: the Router and its M.O.E. Questions¶

In [9]:
moe()

image.png

.

Definition: a Router and its M.O.E. Questions¶

PyTeal's Router constructs the Teal code necessary to delegate application transactions to either a bare app call action or a method based on answers to:¶

[M] In a bare app call? If not, which method selected?¶

[O] Which OnComplete is requested?¶

[E] This app already exists? (Conversely, being created?)¶

image.png

.

image.png

image.png

.

image.png

image.png

.

image.png

image.png

.

Router Initialization¶

image.png

image.png

.

In [10]:
warning()
In [11]:
# WARNING: STUBS ARE FOR ROUTER-ILLUSTRATION PURPOSES ONLY!!!
del_action = OnCompleteAction.call_only(Seq()) 

router = Router(
    name="OpenPollingApp",
    descr="This is a polling application.",
    bare_calls=BareCallActions(delete_application=del_action),
)

image.png

.

In [12]:
approval, clear, json_contract = router.compile_program(version=8)

Compile¶

approval, clear, json_contract = router.compile_program(version=8)

JSON Contract (with no methods)¶

In [13]:
print(json.dumps(json_contract.dictify(), indent=2))
{
  "name": "OpenPollingApp",
  "methods": [],
  "networks": {},
  "desc": "This is a polling application."
}

image.png

.

Compile¶

approval, clear, json_contract = router.compile_program(version=8)
In [14]:
print(approval)
#pragma version 8
txn NumAppArgs
int 0
==
bnz main_l2
err
main_l2:
txn OnCompletion
int DeleteApplication
==
bnz main_l4
err
main_l4:
txn ApplicationID
int 0
!=
assert
int 1
return

image.png

.

In [15]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l2
err
main_l2:
txn OnCompletion
int DeleteApplication
==
bnz main_l4
err
main_l4:
txn ApplicationID
int 0
!=
assert
int 1
return

Routing bare app call delete¶

In [16]:
show.prepare(approval, (2, 5), (7, 11), (13, 19))
In [17]:
show() # custom method showing relevant Teal
txn NumAppArgs
int 0
==
bnz main_l2
. . .
main_l2:
txn OnCompletion
int DeleteApplication
==
bnz main_l4
. . .
main_l4:
txn ApplicationID
int 0
!=
assert
int 1
return

image.png

.

Append methods to route with @router.method¶

image.png

image.png

.

Route open() and close()¶

* no args or output¶

* OnComplete == NoOp and not during create¶

Note:

PyTeal lets us name each method with a the Python reserved word (open and close) via the name param

In [18]:
@router.method(name="open")
def open_poll() -> Expr:
    """Marks this poll as open."""
    return Seq()

@router.method(name="close")
def close_poll() -> Expr:
    """Marks this poll as closed."""
    return Seq()
In [19]:
warning()

image.png

.

Compile¶

In [20]:
approval, clear, json_contract = router.compile_program(version=8)

JSON Contract Stub (with 2 simple method)¶

Now the methods do get filled with information.

In [21]:
print(json.dumps(json_contract.dictify(), indent=2))
{
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "open",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as open."
    },
    {
      "name": "close",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as closed."
    }
  ],
  "networks": {},
  "desc": "This is a polling application."
}
In [22]:
# print(approval)
In [23]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l6
txna ApplicationArgs 0
method "open()void"
==
bnz main_l5
txna ApplicationArgs 0
method "close()void"
==
bnz main_l4
err
main_l4:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_1
int 1
return
main_l5:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_0
int 1
return
main_l6:
txn OnCompletion
int DeleteApplication
==
bnz main_l8
err
main_l8:
txn ApplicationID
int 0
!=
assert
int 1
return

// open
open_0:
retsub

// close
close_1:
retsub
In [24]:
show.prepare(approval, (6, 9), (10, 13), (27, 36), (15, 24), (53, 55), (57, 59))

Notes:

  • method pseudo-opcode computes the method selector (SHA512-256 of the method signature)
  • router ask Q1 by comparing with txna ApplicationArgs 0 (method args start at txn ApplicationArgs 1) and branches to relevant label
  • then asks Q2 & Q3 by asserting thet OnCompletion == NoOp and already existing ApplicatoinId != 0
  • then routes to actual subroutine that implements the method with a callsub

ABI Types¶

image.png

.

A Selection of PyTeal's Basic ABI Types¶

PyTeal Type

ARC-4 Type

Dynamic / Static

Description

abi.Uint8

uint8

Static

An 8-bit unsigned integer

abi.StaticArray[T,N]

T[N]

IFF T is

A fixed-length array with N elements

abi.String

string

Dynamic

Variable-length byte array

There are more!¶

Uint16/32/64, Bool, Byte, DynamicArray, Tuple, ...¶

Link: PyTeal ABI Types
¶

image.png

.

Add a method with ABI arguments¶

Notes

  • utilizes choice: abi.Uint8
  • take a mental note of the Docstring """Submit ... """
    • The very first line becomes the desc field of the method in the JSON contract
In [26]:
@router.method
def submit(choice: abi.Uint8) -> Expr:
    """Submit a response to the poll.
    Args:
        choice: The choice made by the sender.
    """
    return Seq()
In [27]:
warning()

image.png

.

Compile¶

In [28]:
opts = OptimizeOptions(scratch_slots=True)
(approval, clear, json_contract) = \
    router.compile_program(version=8, optimize=opts)
# ------------------------------------^^^^^^^^^^^^^
In [29]:
print(json.dumps(json_contract.dictify(), indent=2))
{
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "open",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as open."
    },
    {
      "name": "close",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as closed."
    },
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice",
          "desc": "The choice made by the sender."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll."
    }
  ],
  "networks": {},
  "desc": "This is a polling application."
}
In [30]:
%%script false --no-raise-error #{
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "open",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as open."
    },
    {
      "name": "close",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as closed."
    },
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice",
          "desc": "The choice made by the sender."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll."
    }
  ],
  "networks": {},
  "desc": "This is a polling application."
}

submit(choice) portion of JSON contract¶

Notes:

  • desc is coming from the defining Python function's Docstring
In [32]:
show() # lines 20-33 of the JSON contract
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice",
          "desc": "The choice made by the sender."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll."
    }

image.png

.

In [34]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l8
txna ApplicationArgs 0
method "open()void"
==
bnz main_l7
txna ApplicationArgs 0
method "close()void"
==
bnz main_l6
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l5
err
main_l5:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_2
int 1
return
main_l6:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_1
int 1
return
main_l7:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_0
int 1
return
main_l8:
txn OnCompletion
int DeleteApplication
==
bnz main_l10
err
main_l10:
txn ApplicationID
int 0
!=
assert
int 1
return

// open
open_0:
retsub

// close
close_1:
retsub

// submit
submit_2:
store 0
retsub
In [35]:
show.prepare(approval, (14, 17), (19, 31), (80, 83))

Routing submit(choice)¶

In [36]:
show()
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l5
. . .
main_l5:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_2
. . .
// submit
submit_2:
store 0
retsub

OMIT THE REST OF THE STUBS FROM PRESENTATION

Notes:

  • @router.method decorator is signalled about looking for an OnComplete==NoOp during an app create
  • introducing types abi.StaticArray[abi.String, Literal[3]], abi.Bool
  • the first is recursively "dynamic type"
    • dynamic means that the size of data being encoded is not fixed
  • ARC-4 specifies exactly how each type is encoded

image.png

.

Compile¶

Approval Program Stub (with 2 simple methods + 1 non-returning OnComplete=NoOp + 1 non-returning during creation)¶

In [39]:
# print(approval)
In [40]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l10
txna ApplicationArgs 0
method "open()void"
==
bnz main_l9
txna ApplicationArgs 0
method "close()void"
==
bnz main_l8
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l7
txna ApplicationArgs 0
method "create(string[3],bool)void"
==
bnz main_l6
err
main_l6:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
==
&&
assert
txna ApplicationArgs 1
store 1
txna ApplicationArgs 2
int 0
int 8
*
getbit
store 2
load 1
load 2
callsub create_3
int 1
return
main_l7:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_2
int 1
return
main_l8:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_1
int 1
return
main_l9:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_0
int 1
return
main_l10:
txn OnCompletion
int DeleteApplication
==
bnz main_l12
err
main_l12:
txn ApplicationID
int 0
!=
assert
int 1
return

// open
open_0:
retsub

// close
close_1:
retsub

// submit
submit_2:
store 0
retsub

// create
create_3:
store 4
store 3
retsub

image.png

.

Demo: https://jasonpaulos.github.io/decipher-22-pyteal-talk/¶

In [43]:
demo_qr_code()

image.png

.

APPENDIX - The Actual Program¶

image.png

.

NOTES:

  • make sure to put the final version here
In [44]:
# This example is provided for informational purposes only and has not been audited for security.
import json
from typing import Literal
from pyteal import *


pragma(compiler_version="0.20.1")

on_delete = Seq(
    Assert(Txn.sender() == Global.creator_address()),
    InnerTxnBuilder.Execute(
        {
            TxnField.type_enum: TxnType.Payment,
            TxnField.close_remainder_to: Txn.sender(),
        }
    ),
)


router = Router(
    name="OpenPollingApp",
    descr="A polling application with no restrictions on who can participate.",
    bare_calls=BareCallActions(
        delete_application=OnCompleteAction.call_only(on_delete)
    ),
)

NUM_OPTIONS = 7

open_key = Bytes(b"open")
resubmit_key = Bytes(b"resubmit")
question_key = Bytes(b"question")
option_name_prefix = b"option_name_"
option_name_keys = [Bytes(option_name_prefix + bytes([i])) for i in range(NUM_OPTIONS)]
option_count_prefix = b"option_count_"
option_count_keys = [
    Bytes(option_count_prefix + bytes([i])) for i in range(NUM_OPTIONS)
]


@router.method(no_op=CallConfig.CREATE)
def create(
    question: abi.String, options: abi.StaticArray[abi.String, Literal[NUM_OPTIONS]], can_resubmit: abi.Bool  # type: ignore[valid-type]
) -> Expr:
    """Create a new polling application.

    Args:
        question: The question this poll is asking.
        options: A list of options for the poll. This list should not contain duplicate entries.
        can_resubmit: Whether this poll allows accounts to change their submissions or not.
    """
    name = abi.make(abi.String)
    return Seq(
        App.globalPut(open_key, Int(0)),
        App.globalPut(resubmit_key, can_resubmit.get()),
        App.globalPut(question_key, question.get()),
        *[
            Seq(
                name.set(options[i]),
                App.globalPut(option_name_keys[i], name.get()),
                App.globalPut(option_count_keys[i], Int(0)),
            )
            for i in range(NUM_OPTIONS)
        ],
    )


@router.method(name="open")
def open_poll() -> Expr:
    """Marks this poll as open.

    This will fail if the poll is already open.

    The poll must be open in order to receive user input.
    """
    return Seq(
        Assert(Not(App.globalGet(open_key))),
        App.globalPut(open_key, Int(1)),
    )


@router.method(name="close")
def close_poll() -> Expr:
    """Marks this poll as closed.

    This will fail if the poll is already closed.
    """
    return Seq(
        Assert(App.globalGet(open_key)),
        App.globalPut(open_key, Int(0)),
    )


@router.method
def submit(choice: abi.Uint8) -> Expr:
    """Submit a response to the poll.

    Submissions can only be received if the poll is open. If the poll is closed, this will fail.

    If a submission has already been made by the sender and the poll allows resubmissions, the
    sender's choice will be updated to the most recent submission. If the poll does not allow
    resubmissions, this action will fail.

    Args:
        choice: The choice made by the sender. This must be an index into the options for this poll.
    """
    new_choice_count_key = ScratchVar(TealType.bytes)
    old_choice_count_key = ScratchVar(TealType.bytes)
    return Seq(
        Assert(choice.get() < Int(NUM_OPTIONS)),
        new_choice_count_key.store(
            SetByte(option_count_keys[0], Int(len(option_count_prefix)), choice.get())
        ),
        sender_box := App.box_get(Txn.sender()),
        If(sender_box.hasValue()).Then(
            # the sender has already submitted a response, so it must be cleared
            Assert(App.globalGet(resubmit_key)),
            old_choice_count_key.store(
                SetByte(
                    option_count_keys[0],
                    Int(len(option_count_prefix)),
                    Btoi(sender_box.value()),
                )
            ),
            App.globalPut(
                old_choice_count_key.load(),
                App.globalGet(old_choice_count_key.load()) - Int(1),
            ),
        ),
        App.box_put(Txn.sender(), choice.encode()),
        App.globalPut(
            new_choice_count_key.load(),
            App.globalGet(new_choice_count_key.load()) + Int(1),
        ),
    )


class PollStatus(abi.NamedTuple):
    question: abi.Field[abi.String]
    can_resubmit: abi.Field[abi.Bool]
    is_open: abi.Field[abi.Bool]
    results: abi.Field[abi.StaticArray[abi.Tuple2[abi.String, abi.Uint64], Literal[NUM_OPTIONS]]]  # type: ignore[valid-type]


@router.method
def status(*, output: PollStatus) -> Expr:
    """Get the status of this poll.

    Returns:
        A tuple containing the following information, in order: the question is poll is asking,
        whether the poll allows resubmission, whether the poll is open, and an array of the poll's
        current results. This array contains one entry per option, and each entry is a tuple of that
        option's value and the number of accounts who have voted for it.
    """
    question = abi.make(abi.String)
    can_resubmit = abi.make(abi.Bool)
    is_open = abi.make(abi.Bool)
    option_name = abi.make(abi.String)
    option_count = abi.make(abi.Uint64)
    partial_results = [
        abi.make(abi.Tuple2[abi.String, abi.Uint64]) for i in range(NUM_OPTIONS)
    ]
    results = abi.make(abi.StaticArray[abi.Tuple2[abi.String, abi.Uint64], Literal[NUM_OPTIONS]])  # type: ignore[valid-type]
    return Seq(
        question.set(App.globalGet(question_key)),
        can_resubmit.set(App.globalGet(resubmit_key)),
        is_open.set(App.globalGet(open_key)),
        *[
            Seq(
                option_name.set(App.globalGet(option_name_keys[i])),
                option_count.set(App.globalGet(option_count_keys[i])),
                partial_results[i].set(option_name, option_count),
            )
            for i in range(NUM_OPTIONS)
        ],
        results.set(partial_results),
        output.set(question, can_resubmit, is_open, results),
    )

contract/contract.py¶

In [47]:
show()
# This example is provided for informational purposes only and has not been audited for security.
import json
from typing import Literal
from pyteal import *


pragma(compiler_version="0.20.1")

on_delete = Seq(
    Assert(Txn.sender() == Global.creator_address()),
    InnerTxnBuilder.Execute(
        {
            TxnField.type_enum: TxnType.Payment,
            TxnField.close_remainder_to: Txn.sender(),
        }
    ),
)


router = Router(
    name="OpenPollingApp",
    descr="A polling application with no restrictions on who can participate.",
    bare_calls=BareCallActions(
        delete_application=OnCompleteAction.call_only(on_delete)
    ),
)

NUM_OPTIONS = 7

open_key = Bytes(b"open")
resubmit_key = Bytes(b"resubmit")
question_key = Bytes(b"question")
option_name_prefix = b"option_name_"
option_name_keys = [Bytes(option_name_prefix + bytes([i])) for i in range(NUM_OPTIONS)]
option_count_prefix = b"option_count_"
option_count_keys = [
    Bytes(option_count_prefix + bytes([i])) for i in range(NUM_OPTIONS)
]


@router.method(no_op=CallConfig.CREATE)
def create(
    question: abi.String, options: abi.StaticArray[abi.String, Literal[NUM_OPTIONS]], can_resubmit: abi.Bool  # type: ignore[valid-type]
) -> Expr:
    """Create a new polling application.

    Args:
        question: The question this poll is asking.
        options: A list of options for the poll. This list should not contain duplicate entries.
        can_resubmit: Whether this poll allows accounts to change their submissions or not.
    """
    name = abi.make(abi.String)
    return Seq(
        App.globalPut(open_key, Int(0)),
        App.globalPut(resubmit_key, can_resubmit.get()),
        App.globalPut(question_key, question.get()),
        *[
            Seq(
                name.set(options[i]),
                App.globalPut(option_name_keys[i], name.get()),
                App.globalPut(option_count_keys[i], Int(0)),
            )
            for i in range(NUM_OPTIONS)
        ],
    )


@router.method(name="open")
def open_poll() -> Expr:
    """Marks this poll as open.

    This will fail if the poll is already open.

    The poll must be open in order to receive user input.
    """
    return Seq(
        Assert(Not(App.globalGet(open_key))),
        App.globalPut(open_key, Int(1)),
    )


@router.method(name="close")
def close_poll() -> Expr:
    """Marks this poll as closed.

    This will fail if the poll is already closed.
    """
    return Seq(
        Assert(App.globalGet(open_key)),
        App.globalPut(open_key, Int(0)),
    )


@router.method
def submit(choice: abi.Uint8) -> Expr:
    """Submit a response to the poll.

    Submissions can only be received if the poll is open. If the poll is closed, this will fail.

    If a submission has already been made by the sender and the poll allows resubmissions, the
    sender's choice will be updated to the most recent submission. If the poll does not allow
    resubmissions, this action will fail.

    Args:
        choice: The choice made by the sender. This must be an index into the options for this poll.
    """
    new_choice_count_key = ScratchVar(TealType.bytes)
    old_choice_count_key = ScratchVar(TealType.bytes)
    return Seq(
        Assert(choice.get() < Int(NUM_OPTIONS)),
        new_choice_count_key.store(
            SetByte(option_count_keys[0], Int(len(option_count_prefix)), choice.get())
        ),
        sender_box := App.box_get(Txn.sender()),
        If(sender_box.hasValue()).Then(
            # the sender has already submitted a response, so it must be cleared
            Assert(App.globalGet(resubmit_key)),
            old_choice_count_key.store(
                SetByte(
                    option_count_keys[0],
                    Int(len(option_count_prefix)),
                    Btoi(sender_box.value()),
                )
            ),
            App.globalPut(
                old_choice_count_key.load(),
                App.globalGet(old_choice_count_key.load()) - Int(1),
            ),
        ),
        App.box_put(Txn.sender(), choice.encode()),
        App.globalPut(
            new_choice_count_key.load(),
            App.globalGet(new_choice_count_key.load()) + Int(1),
        ),
    )


class PollStatus(abi.NamedTuple):
    question: abi.Field[abi.String]
    can_resubmit: abi.Field[abi.Bool]
    is_open: abi.Field[abi.Bool]
    results: abi.Field[abi.StaticArray[abi.Tuple2[abi.String, abi.Uint64], Literal[NUM_OPTIONS]]]  # type: ignore[valid-type]


@router.method
def status(*, output: PollStatus) -> Expr:
    """Get the status of this poll.

    Returns:
        A tuple containing the following information, in order: the question is poll is asking,
        whether the poll allows resubmission, whether the poll is open, and an array of the poll's
        current results. This array contains one entry per option, and each entry is a tuple of that
        option's value and the number of accounts who have voted for it.
    """
    question = abi.make(abi.String)
    can_resubmit = abi.make(abi.Bool)
    is_open = abi.make(abi.Bool)
    option_name = abi.make(abi.String)
    option_count = abi.make(abi.Uint64)
    partial_results = [
        abi.make(abi.Tuple2[abi.String, abi.Uint64]) for i in range(NUM_OPTIONS)
    ]
    results = abi.make(abi.StaticArray[abi.Tuple2[abi.String, abi.Uint64], Literal[NUM_OPTIONS]])  # type: ignore[valid-type]
    return Seq(
        question.set(App.globalGet(question_key)),
        can_resubmit.set(App.globalGet(resubmit_key)),
        is_open.set(App.globalGet(open_key)),
        *[
            Seq(
                option_name.set(App.globalGet(option_name_keys[i])),
                option_count.set(App.globalGet(option_count_keys[i])),
                partial_results[i].set(option_name, option_count),
            )
            for i in range(NUM_OPTIONS)
        ],
        results.set(partial_results),
        output.set(question, can_resubmit, is_open, results),
    )
In [48]:
approval, clear, json_contract = router.compile_program(version=8, optimize=opts)

JSON Contract Stub - COMPLETE¶

In [49]:
# print(json.dumps(json_contract.dictify(), indent=2))
In [50]:
%%script false --no-raise-error # {
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "create",
      "args": [
        {
          "type": "string[3]",
          "name": "options",
          "desc": "A list of options for the poll. This list should not contain duplicate entries."
        },
        {
          "type": "bool",
          "name": "can_resubmit",
          "desc": "Whether this poll allows accounts to change their submissions or not."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Create a new polling application."
    },
    {
      "name": "open",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as open.\nThis will fail if the poll is already open.\nThe poll must be open in order to receive user input."
    },
    {
      "name": "close",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as closed.\nThis will fail if the poll is already closed."
    },
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice",
          "desc": "The choice made by the sender. This must be an index into the options for this poll."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll.\nSubmissions can only be received if the poll is open. If the poll is closed, this will fail.\nIf a submission has already been made by the sender and the poll allows resubmissions, the sender's choice will be updated to the most recent submission. If the poll does not allow resubmissions, this action will fail."
    },
    {
      "name": "status",
      "args": [],
      "returns": {
        "type": "(bool,bool,(string,uint64)[3])",
        "desc": "A tuple containing the following information, in order: whether the poll allows resubmission, whether the poll is open, and an array of the poll's current results. This array contains one entry per option, and each entry is a tuple of that option's value and the number of accounts who have voted for it."
      },
      "desc": "Get the status of this poll."
    }
  ],
  "networks": {},
  "desc": "A polling application with no restrictions on who can participate."
}

see contract/contract.json¶

In [52]:
show()
{
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "create",
      "args": [
        {
          "type": "string",
          "name": "question",
          "desc": "The question this poll is asking."
        },
        {
          "type": "string[7]",
          "name": "options",
          "desc": "A list of options for the poll. This list should not contain duplicate entries."
        },
        {
          "type": "bool",
          "name": "can_resubmit",
          "desc": "Whether this poll allows accounts to change their submissions or not."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Create a new polling application."
    },
    {
      "name": "open",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as open.\nThis will fail if the poll is already open.\nThe poll must be open in order to receive user input."
    },
    {
      "name": "close",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as closed.\nThis will fail if the poll is already closed."
    },
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice",
          "desc": "The choice made by the sender. This must be an index into the options for this poll."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll.\nSubmissions can only be received if the poll is open. If the poll is closed, this will fail.\nIf a submission has already been made by the sender and the poll allows resubmissions, the sender's choice will be updated to the most recent submission. If the poll does not allow resubmissions, this action will fail."
    },
    {
      "name": "status",
      "args": [],
      "returns": {
        "type": "(string,bool,bool,(string,uint64)[7])",
        "desc": "A tuple containing the following information, in order: the question is poll is asking, whether the poll allows resubmission, whether the poll is open, and an array of the poll's current results. This array contains one entry per option, and each entry is a tuple of that option's value and the number of accounts who have voted for it."
      },
      "desc": "Get the status of this poll."
In [53]:
# print(approval)
In [54]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l12
txna ApplicationArgs 0
method "create(string[3],bool)void"
==
bnz main_l11
txna ApplicationArgs 0
method "open()void"
==
bnz main_l10
txna ApplicationArgs 0
method "close()void"
==
bnz main_l9
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l8
txna ApplicationArgs 0
method "status()(bool,bool,(string,uint64)[3])"
==
bnz main_l7
err
main_l7:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub status_4
store 2
byte 0x151f7c75
load 2
concat
log
int 1
return
main_l8:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_3
int 1
return
main_l9:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_2
int 1
return
main_l10:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_1
int 1
return
main_l11:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
==
&&
assert
txna ApplicationArgs 1
store 0
txna ApplicationArgs 2
int 0
int 8
*
getbit
store 1
load 0
load 1
callsub create_0
int 1
return
main_l12:
txn OnCompletion
int DeleteApplication
==
bnz main_l14
err
main_l14:
txn ApplicationID
int 0
!=
assert
txn Sender
global CreatorAddress
==
assert
int 1
return

// create
create_0:
store 20
store 19
byte 0x6f70656e
int 0
app_global_put
byte 0x72657375626d6974
load 20
app_global_put
load 19
load 19
int 2
int 0
*
extract_uint16
int 0
int 1
+
int 3
==
bnz create_0_l8
load 19
int 2
int 0
*
int 2
+
extract_uint16
create_0_l2:
substring3
store 21
byte 0x6f7074696f6e5f6e616d655f00
load 21
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f00
int 0
app_global_put
load 19
load 19
int 2
int 1
*
extract_uint16
int 1
int 1
+
int 3
==
bnz create_0_l7
load 19
int 2
int 1
*
int 2
+
extract_uint16
create_0_l4:
substring3
store 21
byte 0x6f7074696f6e5f6e616d655f01
load 21
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f01
int 0
app_global_put
load 19
load 19
int 2
int 2
*
extract_uint16
int 2
int 1
+
int 3
==
bnz create_0_l6
load 19
int 2
int 2
*
int 2
+
extract_uint16
b create_0_l9
create_0_l6:
load 19
len
b create_0_l9
create_0_l7:
load 19
len
b create_0_l4
create_0_l8:
load 19
len
b create_0_l2
create_0_l9:
substring3
store 21
byte 0x6f7074696f6e5f6e616d655f02
load 21
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f02
int 0
app_global_put
retsub

// open
open_1:
byte 0x6f70656e
app_global_get
!
assert
byte 0x6f70656e
int 1
app_global_put
retsub

// close
close_2:
byte 0x6f70656e
app_global_get
assert
byte 0x6f70656e
int 0
app_global_put
retsub

// submit
submit_3:
store 22
load 22
int 3
<
assert
byte 0x6f7074696f6e5f636f756e745f00
int 13
load 22
setbyte
store 23
txn Sender
box_get
store 26
store 25
load 26
bz submit_3_l2
byte 0x72657375626d6974
app_global_get
assert
byte 0x6f7074696f6e5f636f756e745f00
int 13
load 25
btoi
setbyte
store 24
load 24
load 24
app_global_get
int 1
-
app_global_put
submit_3_l2:
txn Sender
byte 0x00
int 0
load 22
setbyte
box_put
load 23
load 23
app_global_get
int 1
+
app_global_put
retsub

// status
status_4:
byte 0x72657375626d6974
app_global_get
!
!
store 3
byte 0x6f70656e
app_global_get
!
!
store 4
byte 0x6f7074696f6e5f6e616d655f00
app_global_get
store 5
load 5
len
itob
extract 6 0
load 5
concat
store 5
byte 0x6f7074696f6e5f636f756e745f00
app_global_get
store 6
load 5
store 11
int 10
itob
extract 6 0
load 6
itob
concat
load 11
concat
store 7
byte 0x6f7074696f6e5f6e616d655f01
app_global_get
store 5
load 5
len
itob
extract 6 0
load 5
concat
store 5
byte 0x6f7074696f6e5f636f756e745f01
app_global_get
store 6
load 5
store 12
int 10
itob
extract 6 0
load 6
itob
concat
load 12
concat
store 8
byte 0x6f7074696f6e5f6e616d655f02
app_global_get
store 5
load 5
len
itob
extract 6 0
load 5
concat
store 5
byte 0x6f7074696f6e5f636f756e745f02
app_global_get
store 6
load 5
store 13
int 10
itob
extract 6 0
load 6
itob
concat
load 13
concat
store 9
load 7
store 17
load 17
store 16
int 6
store 14
load 14
load 17
len
+
store 15
load 15
int 65536
<
assert
load 14
itob
extract 6 0
load 8
store 17
load 16
load 17
concat
store 16
load 15
store 14
load 14
load 17
len
+
store 15
load 15
int 65536
<
assert
load 14
itob
extract 6 0
concat
load 9
store 17
load 16
load 17
concat
store 16
load 15
store 14
load 14
itob
extract 6 0
concat
load 16
concat
store 10
byte 0x00
int 0
load 3
setbit
int 1
load 4
setbit
load 10
store 18
int 3
itob
extract 6 0
concat
load 18
concat
retsub

see contract/approval.teal¶

In [56]:
show()
#pragma version 8
txn NumAppArgs
int 0
==
bnz main_l12
txna ApplicationArgs 0
method "create(string,string[7],bool)void"
==
bnz main_l11
txna ApplicationArgs 0
method "open()void"
==
bnz main_l10
txna ApplicationArgs 0
method "close()void"
==
bnz main_l9
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l8
txna ApplicationArgs 0
method "status()(string,bool,bool,(string,uint64)[7])"
==
bnz main_l7
err
main_l7:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub status_4
store 3
byte 0x151f7c75
load 3
concat
log
int 1
return
main_l8:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_3
int 1
return
main_l9:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_2
int 1
return
main_l10:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_1
int 1
return
main_l11:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
==
&&
assert
txna ApplicationArgs 1
store 0
txna ApplicationArgs 2
store 1
txna ApplicationArgs 3
int 0
int 8
*
getbit
store 2
load 0
load 1
load 2
callsub create_0
int 1
return
main_l12:
txn OnCompletion
int DeleteApplication
==
bnz main_l14
err
main_l14:
txn ApplicationID
int 0
!=
assert
txn Sender
global CreatorAddress
==
assert
itxn_begin
int pay
itxn_field TypeEnum
txn Sender
itxn_field CloseRemainderTo
itxn_submit
int 1
return

// create
create_0:
store 34
store 33
store 32
byte 0x6f70656e
int 0
app_global_put
byte 0x72657375626d6974
load 34
app_global_put
byte 0x7175657374696f6e
load 32
extract 2 0
app_global_put
load 33
load 33
int 2
int 0
*
extract_uint16
int 0
int 1
+
int 7
==
bnz create_0_l20
load 33
int 2
int 0
*
int 2
+
extract_uint16
create_0_l2:
substring3
store 35
byte 0x6f7074696f6e5f6e616d655f00
load 35
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f00
int 0
app_global_put
load 33
load 33
int 2
int 1
*
extract_uint16
int 1
int 1
+
int 7
==
bnz create_0_l19
load 33
int 2
int 1
*
int 2
+
extract_uint16
create_0_l4:
substring3
store 35
byte 0x6f7074696f6e5f6e616d655f01
load 35
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f01
int 0
app_global_put
load 33
load 33
int 2
int 2
*
extract_uint16
int 2
int 1
+
int 7
==
bnz create_0_l18
load 33
int 2
int 2
*
int 2
+
extract_uint16
create_0_l6:
substring3
store 35
byte 0x6f7074696f6e5f6e616d655f02
load 35
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f02
int 0
app_global_put
load 33
load 33
int 2
int 3
*
extract_uint16
int 3
int 1
+
int 7
==
bnz create_0_l17
load 33
int 2
int 3
*
int 2
+
extract_uint16
create_0_l8:
substring3
store 35
byte 0x6f7074696f6e5f6e616d655f03
load 35
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f03
int 0
app_global_put
load 33
load 33
int 2
int 4
*
extract_uint16
int 4
int 1
+
int 7
==
bnz create_0_l16
load 33
int 2
int 4
*
int 2
+
extract_uint16
create_0_l10:
substring3
store 35
byte 0x6f7074696f6e5f6e616d655f04
load 35
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f04
int 0
app_global_put
load 33
load 33
int 2
int 5
*
extract_uint16
int 5
int 1
+
int 7
==
bnz create_0_l15
load 33
int 2
int 5
*
int 2
+
extract_uint16
create_0_l12:
substring3
store 35
byte 0x6f7074696f6e5f6e616d655f05
load 35
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f05
int 0
app_global_put
load 33
load 33
int 2
int 6
*
extract_uint16
int 6
int 1
+
int 7
==
bnz create_0_l14
load 33
int 2
int 6
*
int 2
+
extract_uint16
b create_0_l21
create_0_l14:
load 33
len
b create_0_l21
create_0_l15:
load 33
len
b create_0_l12
create_0_l16:
load 33
len
b create_0_l10
create_0_l17:
load 33
len
b create_0_l8
create_0_l18:
load 33
len
b create_0_l6
create_0_l19:
load 33
len
b create_0_l4
create_0_l20:
load 33
len
b create_0_l2
create_0_l21:
substring3
store 35
byte 0x6f7074696f6e5f6e616d655f06
load 35
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f06
int 0
app_global_put
retsub

// open
open_1:
byte 0x6f70656e
app_global_get
!
assert
byte 0x6f70656e
int 1
app_global_put
retsub

// close
close_2:
byte 0x6f70656e
app_global_get
assert
byte 0x6f70656e
int 0
app_global_put
retsub

// submit
submit_3:
store 36
load 36
int 7
<
assert
byte 0x6f7074696f6e5f636f756e745f00
int 13
load 36
setbyte
store 37
txn Sender
box_get
store 40
store 39
load 40
bz submit_3_l2
byte 0x72657375626d6974
app_global_get
assert
byte 0x6f7074696f6e5f636f756e745f00
int 13
load 39
btoi
setbyte
store 38
load 38
load 38
app_global_get
int 1
-
app_global_put
submit_3_l2:
txn Sender
byte 0x00
int 0
load 36
setbyte
box_put
load 37
load 37
app_global_get
int 1
+
app_global_put
retsub

// status
status_4:
byte 0x7175657374696f6e
app_global_get
store 4
load 4
len
itob